design sweep 2: work the ranked backlog - #214
Conversation
…etry Two defects that shared a shape: a message the user could never get to. - The error boundary's "Try again" called `reset()` alone, which re-runs the segment from the client router cache -- still holding the payload that threw. A server-side failure therefore rendered the same error forever. Pairs it with `router.refresh()` so the cached payload is discarded first. - Native constraint validation runs before the `submit` event, and React's `<form action>` runs from it, so `required` / `type="url"` / `max=` on the new-operation form made its server-side rejections unreachable. Removes the native attributes that shadowed a server message and corrects the copy: a bare hostname fails `new URL()` outright and yields `url_invalid`, not the `url_scheme` the old text described.
Below 30rem the sticky name pin measured 24% of the region, so the column it anchored was wider than the space left to read it in. The row now reflows to labelled blocks, the way `.log--crew` already does, and `.payouts__label` / `.roster__label` name the columns the missing `<thead>` no longer can. Sibling classes rather than a reuse of `.crew__label`: the two tables reflow at the same breakpoint today and a shared class would weld that together. The register entry is the part worth sharing, and it is shared. The pin's ratio assertion moves from 320/390px to 560px, where there is still a pin to measure -- a `<td>` laid out as a block has no scroll range.
Access lists: "Check now" rendered in every state, including the one a fresh deployment opens on -- no holder, so the job returns at its first branch having read nothing, and the admin was told "Check queued at 09:41:22.418 UTC" and reloaded to a byte-identical page. The confirmation was true about the enqueue and false about everything they cared about. Gated on `remedy.kind === "check-now"`, which is tighter than `showsObservations(state)`: the job cannot read in the three holder-fault states either, and an e2e assertion on state 3 pins that divergence. Payouts detail: eight confirm controls whose actions revalidate rather than redirect, so a press moved nothing on screen for the whole round trip while `useSubmitGuard` silently refused every further press -- and with no `ConfirmGroup` on this route there is no channel for the refusal. Each now carries a `pendingLabel`, which is where the operator is already looking. `LifecycleSubmit` takes it as a required prop so a new lifecycle control has to answer the question rather than inherit the gap. `ConfirmSubmit`'s ghost-label width reservation was widened to include a string `pendingLabel` first, so the new labels cannot reflow a button mid-press. Also carries the roster's `Shares` per-cell label, which cannot be split from this commit without leaving a tree that does not compile.
…answer The audit log writes a member as `actor` only for what they did to their own account. Everything done TO them -- role changes, tier moves, unlinks -- is written with `system` or an admin as actor and the member as target, and that is most of the log. The page already knew this and said so, from inside `emptyMessage`'s filtered branch: it fired on zero rows, the one outcome that has already told the admin something is wrong. The outcome that does the damage is the other one -- an actor filter that returns the member's handful of self-service entries, reads as a complete history, and is not one. Same sentence, hoisted above the rows so both cases see it. Still gated on actor-set-and-target-unset, the only shape it can help. Rendered as a plain `.lede`, not a Notice: nothing here is wrong, the filter is just pointed at the column that answers a different question, and a warn band would say otherwise. e2e/audit.spec.ts asserted the old placement; rewritten to walk all four states -- no filter, actor filter empty, actor filter with a row, both columns crossed.
Six rows at one identical weight, of which exactly one -- write_contacts -- writes anything at all, sitting second of six with nothing marking it out. And six sentences that each re-established "so authGD can show your location", teaching the reader that these lines do not differ, on the one screen where the differences are the entire point. crewNorms' shape from account/page.tsx: the shared fact once in the group's head, the rows carrying only what differs. Three groups -- contacts (the only thing authGD writes), location, running client -- each a <dl> of its own, so the head is adjacent to exactly the rows it describes in both channels rather than being a <p> stranded inside a single list with no defined relationship to either side of it. Grouping reorders relative to EVE_SSO_SCOPES on purpose: config order is an ops concern and means nothing to the person deciding. A scope the list does not place still renders, last, under a head that admits the deployment added it; empty groups do not render at all. The head is --ink at weight 600 and sentence case -- deliberately not a fourth member of the uppercase label register, which is for fixed field names, not sentences. That makes it the brightest text in the disclosure, which is the ordering wanted: the answer to "does this change my stuff" lands before any identifier. Two more, same surface: - `.launch__panel > .notice` gets a margin-top. `.notice` carries only a margin-bottom, right everywhere it follows a `.page__head` and wrong here, where it follows the emblem/title/motto stack directly and sat flush against the motto. A member bounced back by an expired session met the reason for it welded to the brand mark. Scoped to the child selector so the six other surfaces are untouched, and `.notice-slot` -- the empty live-region reservation this page mounts unconditionally -- is a different class and still draws nothing. - The emblem gets an explicit fetchPriority="low". It is 79.8 KB of decorative WebP with alt=""; the control below it is 2,248 bytes carrying an explicit high. Left unset the emblem contends for the same connection on a first visit over a bad link, and the page's one actionable thing waits behind its own background. e2e/login.spec.ts updated for the new order and wording, plus assertions on the group heads and on head > description > identifier luminance. The fallback group is not covered: EVE_SSO_SCOPES is fixed by playwright.config.ts, so an unplaced scope cannot be rendered from a spec. The head count of 3 pins that it does not appear for the deployed set.
Pattern 2 of the sweep, two of its five instances. payment-history: a real payout is settled in one sitting by one operator, so six rows read `2026-08-10 11:57:07 UTC paid 288,600,000.00 ISK by Fleet Commander` six times over, and the two things an operator is scanning for — the clock and the amount — are the two smallest fragments of the line. The norms are measured against the set (crewNorms' shape from account/page.tsx): a field is dropped only when every event agrees, so one reverted row among five paid ones puts `kind` back on all of them rather than removing it from four. The single-payment case renders inline with no drawer and therefore has no head to hoist into, so it keeps the whole sentence. audit: the timestamp column is pinned, 19 characters wide, and eleven of them restate a date on a page whose rows almost always fall on one day. When they all agree the day is stated once above the table and the cells render a bare clock; when they do not, nothing is hoisted and every row carries its own. Both channels lose exactly the same words in both places. The shared line renders in the flow, not in a `visually-hidden`, and `.only-wide` is a display toggle so the trimmed audit cell is what AT reads at that width too — R4 is parity in both directions, and a per-row assistive-only restoration would be the breach it exists to name. The audit spec's two width assertions accepted only the full stamp; they now accept either rendering, since which one appears is the new test's subject and not theirs. That test drives both branches, because the interesting failure is not the hoist but the hoist firing on rows that disagree — which would delete a date the admin needs and state a false one above it. Verified: npm run typecheck clean; npm run format:check clean; vitest tests/payment-history.test.ts 5 passed; playwright e2e/audit.spec.ts 38 passed; e2e/payouts.spec.ts 103 passed.
Item 5 — /admin/sync's housekeeping summary has never fitted at 320px. The filed fix was to add `.strip__group-disc > summary` to the `@media (max-width: 46rem)` block so it gains `flex-wrap: wrap` the way `.strip__disc > summary` does. That would have done nothing: the summary holds exactly one child (page.tsx:1086-1094, a single `<Status>`), and wrapping a flex container with one item wraps nothing. The nowrap is on `.st` (globals.css:2486-2497), which is written for a one-word token in a table cell and is right everywhere else it lands. So the wrap is granted to the Status, scoped to this summary, plus `align-items: baseline` on the summary itself — with the sentence on three lines, the base rule's `center` puts the `+` marker halfway down the block instead of beside the line it opens. The absolute-gutter treatment `.strip__disc > summary` takes was not copied: that exists because `.strip__name` claims a 100% basis and strands the marker, and nothing here does. Not truncated. The flagged member names are the only thing the collapsed line exists to say. Item 19 — four page-level disclosures took the 28px in-row grade. `.disc > summary` raised to `min-height: 2.25rem`. R1 scopes the 28px grade by the reason for it and states the consequence outright: "A disclosure drawer is not in-row for this purpose and takes 36px." `className="disc"` has exactly four call sites, all page-level sections of /payouts/[id] (page.tsx:135, :844, :1165, appraise-form.tsx:183). PaymentHistory is unaffected and should be — `Disclosure` passes `className` through with no default (disclosure.tsx:144), that call site passes none, and `.log summary` styles it where 28px is correct. Verification, all run: npm run typecheck clean npm run format:check All matched files use Prettier code style! playwright e2e/sync.spec.ts 33 passed playwright e2e/payouts.spec.ts 104 passed The 320px test is self-verifying: it puts `.st`'s own nowrap back on the element and asserts the line then demands more width than it was given, so it fails if the rule is dropped and also fails if the rule never did anything. Confirmed against the unfixed stylesheet: 340.86px of line in a 320px viewport. The containment assertion measures the `.st`, not the `<summary>` — an overflowing flex item spills past its container without growing it, so asserting on the summary's right edge passes vacuously.
Item 7 — `Notice` renders a `<p>` inside a `display: flex` container, so
every top-level child of the message became its own flex item, laid out in
the row box independently of the inline content it contained. A caller
writing `Quote <code>{digest}</code> when you report this` got the `<code>`
promoted to item 2 and the two text runs to items 3 and 4, and the rendered
order read "Quote 4292868890 . when you report this". Visual order diverging
from DOM order is SC 1.3.2, and it is invisible in the source. `{children}`
is now wrapped in one `<span>`, which fixes every `Notice` in the app
carrying inline markup at once.
Item 10 — `/admin/audit` had a static title, so filtering and paging changed
the whole result set without changing anything an admin could hear. The
filed fix was `aria-live`; it does not work here. Every state change on that
surface is a document load (`<form method="get">`, plain `<a href>` pagers),
and a live region arriving with its document is not a mutation, so it
announces nothing while testing green under an attribute-presence assertion.
`generateMetadata` instead: the title says "filtered", "older", or both. It
applies the same finite-number guard to `before` that the page body does, so
a junk cursor cannot make the title claim a page the admin is not on.
Item 11 — `/admin/access-lists` was the one `id="main"` of eleven without
`tabIndex={-1}`, having landed after the sweep that added the rest. A
fragment link moves focus only to elements the platform already considers
focusable, so the skip link scrolled without moving the caret and the next
Tab walked back through the nav (SC 2.4.1).
Item 18 — two row disclosures derived their accessible name from contents
that included `RelativeTime`, a client component on a shared 30s ticker. The
controls renamed themselves twice a minute with no state having changed:
SC 4.1.2 in the announcement channel, and SC 3.2.4 for a voice user whose
"click Alliance ACL 3 to add 2 minutes ago" stops matching what the page now
calls it. Both get a server-computed `ariaLabel`. Note that `aria-label` on
a `<summary>` replaces the computed name outright, so everything stable the
contents were contributing is restated by hand — dropping it would be R4
inverted, deleting information from the assistive channel only. Only the
timestamp is left out, which is the entire point.
Also fixes a regression from 9215e1f: that commit inserted a comment block
between login's `eslint-disable-next-line` and the `<img>` it guards. "Next
line" means the next line and a comment is a line, so the directive
retargeted onto the comment and unguarded the element. Caught by lint, not
by review.
Tests. Three of the four fixes needed a control experiment, because the
obvious assertion for each passes whether or not the defect exists:
- The stranded-period measurement in error-boundary.spec.ts was run against
a restored `display: contents` — "Expected: < 2, Received: 12", 1 failed.
That also convicts the two `toContainText`/`toBeVisible` assertions that
were already there: DOM text order never changed, so they could not have
seen this.
- The skip-link test was run with `tabIndex={-1}` removed —
`Expected: "main", Received: ""`.
- The two rename-stability tests are self-guarding: without the prop
`getAttribute` returns null, which the non-empty precondition catches, and
each asserts the `.ago` text changed so a page where the clock never
ticked cannot pass vacuously. They test stability only; what the name
*contains* stays pinned by the existing `toHaveAccessibleName` cases.
Verification, all run:
npm run typecheck clean
npm run lint clean (was 2 warnings before the login fix)
npm run format:check All matched files use Prettier code style!
npx playwright test e2e/{sync,audit,access-lists,error-boundary,login}.spec.ts
99 passed (1.3m)
…k like
Item 17. `globals.css` keeps one list of selectors that share the label
register — mono, `--t-label`, uppercase, 600 — and DESIGN.md states it in
absolute terms. `.status-line__label` was shaped exactly like a member and
was not one: the only register-shaped selector in the file that never
declared a weight, so it fell to the 400 default.
It surfaces on /account because both channels render the same words. The
rail's `.facts dt` says STANDINGS and MAP; the manifest's status stack says
STANDINGS and MAP. Same family, same size, same colour, about 430px apart,
two different weights. Nothing tells a reader the difference is meaningless,
so the page implies a hierarchy it does not have — which is the precise
collision the register block was written to close.
Added to the list; the rule keeps only its own `letter-spacing` and `color`,
which is what every other member also keeps for itself. Three duplicated
properties go. (SYNTHESIS says four — it counted `letter-spacing` or `color`
as shared, and they are not.)
No layout cost, and the reason is worth recording: the register is
monospace, and IBM Plex Mono ships 400 and 600 at the same advance width, so
weight is free here in a way it would not be in the body face. Measured
rather than assumed — the STATUS cell at 320px forced scroll reads
223.140625px, identical to the figure already documented at
account.spec.ts:1487. The 275-299px range documented at :146 and the 232px
gate are all untouched, so no prose went stale.
That identical measurement is also why the fix needed proving rather than
inferring: "the width did not move" is exactly what a change that never took
effect looks like. Probed the computed style directly and got 600.
New test asserts parity between the two channels rather than the literal
600, because parity is the requirement and the constant is only today's
value of it — if the register moves, the test should follow it rather than
fail. It reads both from the live page, since the way this bug arrived is
that every property present was individually correct and one was simply
absent, which no source-level read of the rule notices. Control experiment:
with the CSS reverted, "Expected: 600, Received: 400".
Verification, all run:
npm run typecheck clean
npm run lint clean
npm run format:check All matched files use Prettier code style!
npx playwright test e2e/{account,location}.spec.ts
86 passed (56.3s)
Item 14 of the sweep, error boundary only. Both controls in `.btn-row` are the plain grade and sit 8px apart, so position is the only thing separating them — and the control in first place, where a reader takes the offered answer to be, was the one the lede directly above had just warned about: "your action may already have taken effect; check before sending it again", followed immediately by the control that sends it again. Reversed the two rather than taking the synthesis's first-listed fix. `.btn--quiet` on Try again is unusable here: that class carries `min-height: 1.75rem`, and DESIGN.md R1 scopes the 28px grade by the reason for it — rows that each carry a control set and are read many at a time. Two buttons read once are not that, so the quiet class would have bought the differentiation by violating the grade rule. Recorded because the same objection blocks the same fix on /admin/sync's Refresh. Not differentiated upward: gold is the page's one emphasis ration, and a boundary has no action it can recommend, so spending it on either control would be a claim this page cannot make. Gold on Try again stays settled and unreopened. Order is the free axis, and it fixes the tab order by the same move. Regression test asserts DOM order via compareDocumentPosition rather than x-coordinates: `.btn-row` wraps at narrow widths, so the leftmost control is not stably the first one, and the tab order follows the DOM either way. Control experiment, because five assertions this sweep have been vacuous: with only src/app/error.tsx reverted, the spec fails — Error: Try again sits before the escape route Expected: true / Received: false Restored, it passes. Verification, all run: npm run typecheck clean npm run lint clean npm run format:check All matched files use Prettier code style! playwright e2e/error-boundary.spec.ts 8 passed (8.2s)
… the same Item 14 of the sweep, second and third instances. `error.tsx` was the first (ec10520); this closes /admin/sync and /payouts/[id]. /admin/sync: Refresh sat in `.btn-row--controls` as a third peer, an `<a href>` with no pending state drawn identically to `Recheck invalid affiliations` 8px away, which enqueues a job. One changes nothing, one puts work on the queue, and only the label separated them. It now sits in `RuleHead`'s aside beside the "checked … UTC" stamp — its actual subject — leaving the control row holding exactly the two things that enqueue. Differentiated by adjacency, not grade: `.btn--quiet` carries `min-height: 1.75rem` and DESIGN.md R1 (:364-369) scopes that 28px grade by the reason for it — rows each carrying a control set, read many at a time. A single control in a section header is not that. Gold is spent on `Sync now`; a second would flatten the first. `view.ts`'s `queuedNotice` docblock argued for "reload this page" over "use Refresh" on distance — Refresh being at the foot below seven rows. My move invalidated that reason, so the docblock is rewritten to stand on the surviving one (a reload needs no lookup and works whether or not the anchor rendered). /payouts/[id]: 70 pressable things, 62 of them the identical 28px quiet chip. New `prominentTrigger` prop on `InlineEdit` raises the trigger to a plain `.btn`, applied to the operation's name and date only — the two fields that are the record's identity rather than a field of it, and the only two above the fold. Deliberately not all five `standalone` uses: promoting five of 62 makes a second uniform tier, the same defect one notch up. Costs no layout — bare `.btn` already carries the `min-height: 2.25rem` that `.inline-edit--standalone .btn--quiet` buys back — so it changes fill and border only, and adds no second gold. layout.tsx: `themeColor` still held `#080f1f`, a navy from a palette the app no longer uses, while `--void` is `oklch(0.145 0 58)`. On mobile that painted blue browser chrome above a neutral page. Corrected to `#0a0a0a`: chroma 0 means equal channels, 0.145³ = 0.00304862 is below the 0.0031308 sRGB knee, so 12.92 × 0.00304862 × 255 = 10.04 = 0x0a. This is drift in a hardcoded duplicate the viewport meta cannot read dynamically, not a palette change — the token is untouched. NOT DONE, deliberately: the synthesis also asked to demote the per-row `exclude` control on /payouts/[id]. Every control in that action cell (page.tsx:1020-1100) is already `btn btn--quiet btn--micro`, which is the floor. Reaching lower needs a new CSS grade below the one R1 already scopes, so it is recorded rather than invented. Verification, all run: - typecheck: clean - lint: clean - format:check: "All matched files use Prettier code style!" - e2e/sync.spec.ts: 35 passed (29.5s) - e2e/payouts.spec.ts: 105 passed (1.6m) Control experiments on both new tests, per the discipline this sweep has been using — the fix reverted, the test re-run, the failure confirmed: - sync: `Expect "toBeVisible" ... waiting for locator('.rule-head__aside') .getByRole('link', { name: 'Refresh' })` — 1 failed - payouts: `locator resolved to <button ... class="btn btn--quiet btn--micro"> - unexpected value "btn btn--quiet btn--micro"` — 1 failed The payouts test's `notRaised` counterweight is `edit battle report URL` alone. Corp share was tried first and failed: its row only renders once a loot pool exists, and seeding one would test the fixture rather than the grade.
…ed ones
Item 20 of the sweep: comments and design-record lines asserting something the
code or the palette contradicts. Nothing here changes a rendered pixel except
one CSS rule; the rest is the record catching up to what is true.
The contrast numbers were all re-measured, and the instrument mattered more
than the arithmetic. `getComputedStyle` hands these tokens back as `oklch(...)`
— still in their authoring space — so the obvious probe, which parses the
string for numbers, reads L, a and b as r, g and b. It reported `--ink`, a
near-white, as `#5c0005`, and every ratio in the palette as between 1.01 and
1.47, without failing. Painting each token to a 1x1 canvas and reading the
pixel back is an actual sRGB conversion; those are the numbers below. The first
run's output was discarded, not filed.
--void #0a0a0a --hull #151514 --hull-hi #21201f
--rule #373533 --rule-strong #787370
--ink #ece7de --ink-dim #bab3a9 --ink-faint #90877e --gold #f1c035
What the record claimed, and what it measures:
--ink-faint on --hull 5.58 -> 5.18 (globals.css x2)
--ink-faint on --hull-hi 4.85 -> 4.61 (globals.css x2, DESIGN.md)
--rule-strong on the three 4.24/3.92/3.48 -> 4.23/3.90/3.47
...and at a second site 4.11/3.72/3.24 -> same
--rule on the three 1.76/1.59/1.39 -> 1.62/1.50/1.33
No floor moves. 5.18 and 4.61 both clear WCAG's 4.5:1 for text, and
--rule-strong still clears 3:1 on every ground a control edge can sit on, which
is the whole reason its lightness is pinned. Every argument these numbers were
quoted in support of survives; only the digits were wrong. Colour tokens are
untouched.
Two of those were already contradicting each other in-tree. `--ink-faint` on
--hull-hi read 4.85 at three sites and 4.63 at two others; the measurement
resolves it at 4.61, so the 4.63 sites were right and the 4.85 ones were not.
`--rule-strong` carried 4.24/3.92/3.48 at its own definition and 4.11/3.72/3.24
sixty lines from the rule that cites it. All sites now quote one measurement,
and the file header records how it was taken so the next re-measure is
comparable rather than a fresh guess with a fresh instrument.
The other claims, each checked against the code rather than against the report
that flagged it:
- `focus-heading.tsx` said a programmatic focus draws no ring, because
`:focus-visible` cannot match one. Chromium does match it: measured
`matches(":focus-visible") === true` and a 2px solid outline painted. The
belief had been copied into `confirm-notice.tsx`, so both are corrected.
The ring is kept — it is the sighted half of the announcement those
components exist to make, and on a soft navigation into a boundary the focus
move is the entire point. What was wrong was its size: an `h1` is a block, so
the ring spanned the column, 912px around 377px of text. That reads as a
page-wide gold band, not as a focus indicator.
`h1[tabindex="-1"] { width: fit-content }` hugs the text. Unconditional
rather than `:focus`-scoped so nothing shifts as focus lands, and it costs no
wrapping — `fit-content` is `min(max-content, max(min-content, available))`,
so wherever the heading would wrap it is still the available width and
`text-wrap: balance` is unaffected. `ConfirmNotice` gets no such rule: it
wraps a full-width panel, where a full-width ring is the right size.
This is the one behavioural change in the commit, and it is the one thing
here a comment correction turned into a real defect fix.
- `nav-items.ts` said `navFromPath` serves "the three surfaces". It serves one.
Both `not-found.tsx` files pass hardcoded `navFor(...)` literals instead — a
404 knows its route statically in a way a shared error boundary does not. A
reader trusting "three" goes looking for two call sites that never existed.
- `submit-guard.ts` justified refusing `disabled` with "every one of these
actions ends in a server-action `redirect()`". `syncJobAction`
(admin/sync/actions.ts:67) revalidates and returns, driven by a `<Submit>`
like the rest. The narrower claim is true and covers both shapes: neither a
client `redirect()` nor a `revalidatePath()` restores focus. The wrong
version invited the wrong repair — adding redirects to the actions that lack
one would not have helped.
- `.launch__scopes dd` said an unrecognised scope "renders no dd at all".
`describeScope`'s default branch returns a sentence saying the deployment
asks for the scope, authGD cannot describe it, and to ask whoever runs it.
That is the better behaviour as well as the actual one, and login/page.tsx
points readers at this comment by name, so the false claim was signposted.
- `.btn-row--controls` described its position as "follows the data it operates
on". True on /admin/sync; on /admin/access-lists it is the first thing under
the H1, above the data, because the control it holds is a remedy for a list
that may not have rendered. The margin is the rule, the placement is the call
site's.
- `confirm-submit.tsx` claimed no caller keeps one class in both states. Five
do, by construction — `armedClassName` is optional. The claim that holds is
the narrower one about `.btn--danger` at rest.
- `payouts/new/page.tsx` credited `--hull` for boxing the form. `--hull` on
`--void` is 1.08:1. `.form-panel`'s 1px `--rule-strong` border, at 4.23:1,
draws the box; the ground sets field contrast without competing. They are
load-bearing in opposite directions, and dropping the border to "flatten" the
panel would delete the box rather than soften it.
Verification. `npm run typecheck`, `npm run lint` and `npm run format:check`
all clean. `e2e/not-found.spec.ts` and `e2e/login.spec.ts`: 12 passed. The new
`fit-content` test was control-verified by reverting only the rule, keeping the
test — it failed on `expect(912).toBeLessThanOrEqual(379)`, which is the exact
before-measurement the comment cites, then passed again on restore. Stashing
the whole change would not have been a control here: it removes the test along
with the fix.
Not done: the 65% / 3.24:1 / 2.88:1 figures in the `.btn:disabled` comment and
DESIGN.md's matching bullet describe an opacity that no longer exists in the
code. They are a historical justification for why the explicit colour replaced
it, the direction of the argument is right, and there is nothing live left to
measure them against.
Item 21, the record-contradiction reviewer's remaining findings. Every one is a written rule that the implementation has never followed — and in five of the six the implementation is right and the rule is wrong, which is the direction that makes these worth fixing rather than filing. A rule nobody obeys is not harmless: it is an instruction to break working code, sitting in the file a new contributor reads first. - **Gold ration.** The rules list said "one primary action per view, plus the mark" — two uses. The `--gold` token's own row, twenty-five lines above, lists four: brand mark, active nav, primary action, Member tier. The app matches the four. Someone auditing a screen against the short version finds gold on the nav and on a tier badge and has to pick which is the mistake, when neither is. The focus ring is gold too and is now called out as exempt — WCAG 2.4.7 puts it wherever focus lands, and an indicator cannot be held to a budget. - **"No decorative gradients at all."** Two `linear-gradient`s exist. Neither is decorative, which is why they survived: `.scroller-fade--start` / `--end` are the only rest-state cue that a table continues sideways, and `.status__dot`'s neutral bar is a gradient of one colour against itself — a flat fill, used so the 0.15em mark sits in a constant-height box and every tone shares a baseline. The rule now says what it means. Read literally it asked for a deleted scroll affordance and a misaligned status row. - **Nav membership.** DESIGN.md listed five destinations; `navFor` returns six. `Access lists` was missing. The module's own docblock has had all six since it shipped, so this was the record lagging the code by one feature. - **`--signal-ok`.** Declared, documented, consumed by nothing, and both DESIGN.md and globals.css pointed at `.notice--ok` as the place it lives. That class has never existed. An editor following the citation finds two comments naming it and no rule. Kept rather than deleted, and both sites now say why: the notice family has a visible gap where an ok variant goes, and `--tier-alumni` holds the same value, so deleting the token leaves the next ok notice reaching for a tier colour. - **`--dur-move`.** Also dead, and the reason is more interesting than the fact. All ten `transition` declarations animate a colour, a border or an opacity, and every one takes `--dur-color`. Transforms do exist — `seal-settle` at 620ms, `btn-pulse` and `link-pending-pulse` at 900ms — but they are `@keyframes` and each names its own duration, because a settle and a loop are not paced like a hover. So the 220ms step is a reservation for the first transform *transition*, and both the token and the motion section now say that instead of implying the scale is in use. Checked rather than assumed: an earlier draft of this commit said "nothing in the shipped UI moves", which the three keyframes disprove. - **"Never a large file scaled down."** PRODUCT.md's fifth principle banned exactly what the account page does and should keep doing. `hero-account.webp` is a 1120px master drawn at 420, or 260 under `.closing--compact`. That oversampling is what keeps it crisp at 2x. The failure the principle exists to prevent is the other direction — an asset stretched past its cut, or a fragment cropped for decoration — and stating it as a downscaling ban condemned the correct implementation while leaving the real failure unnamed. Two comments repeated the inversion: `.closing` claimed the lander was "drawn at the size it was cut for", and the account page claimed the compact variant asked for "a smaller frame rather than downscaling", which describes the same operation as its own opposite. Two live changes, both provably inert: `letter-spacing: 0.12em` at `.tier--lead` and `.launch__foot` becomes `var(--track-label)`, which is declared as `0.12em`. Same computed value, so a test asserting the rendered tracking would pass with or without the edit and prove nothing — the argument is the substitution, not a measurement. Everything else in this commit is prose. `npm run typecheck`, `npm run lint` and `npm run format:check` all clean. `e2e/account.spec.ts`: 82 passed. Not done: `<Image>` on the account page declares 1120x711 with no `sizes`, so a 260px frame still fetches the full master. That is a real cost and it is a code change with a visual risk, not a record correction, so it stays out of an item-21 commit rather than riding along in one.
… a fix Item 22. Record-only: no code, no lint rule, nothing that runs. It is a paragraph because the failure it prevents is one of judgement, and there is no static check that can tell a load-bearing assertion from a vacuous one. The finding behind it: six assertions written during this design sweep passed for the wrong reason, and every one was caught by removing the fix and watching the test stay green. Review caught none of them. That is the whole argument — a new test that passes tells you nothing about whether it can fail, and a green run reports "the fix works" and "the assertion is inert" in identical words. The section also records the instrument, because getting that wrong is how the check silently no-ops: reverting the whole uncommitted change is not a control when the test is part of that change. Stashing takes the test away with the fix, nothing runs, and the resulting green is read as confirmation. The revert has to be surgical — the code under test only, assertion left standing. Three worked examples from this repo, since the failure mode is easier to recognise than to describe: `toBeVisible()` against `.visually-hidden` (a 1px clipped box still has a bounding box), a row-count assertion on a table whose empty state is also one `<tr>`, and a colour assertion parsing `getComputedStyle` output when these tokens serialize as `oklch(...)` and a naive parse silently reads lightness as red. `npm run format:check` clean. Nothing else to run — the file is prose.
Twelve cases, 26 PNGs at 1440x900 and 390x844, from one temporary spec that is deleted again. Both gaps were the same complaint: findings reasoned from markup, on states nobody had rendered. Gap 3 closes clean and that is the result. /login?error= paints the two recoverable codes neutral and the two failures red, and an unrecognised code paints nothing at all rather than an empty bordered box. loginErrorTone's docblock argued this; the shots are the first evidence for it. Gap 5 does not close clean. Four findings, three of which are the sweep's own named patterns arriving together on one page: - Every fault state is an unshaped field — a ~1200px rule labelling a ~545px notice with 500px of void beneath it. - showsObservations excludes only the two states with no holder, so four broken-holder states still print "No lists are being watched yet." under a lede that already explained the fault. - The missing-access table recites "Null Harvest Inc" on 8 of 10 rows, and four broad-grant lines end in the same seven words. crewNorms applies. - STOP WATCHING renders at rule-label weight, identical per row, and is the only destructive control on the page. Plus one plain defect: a watched-but-never-read list renders as "#4104" though the catalog holds its name and catalog is already in scope. watchedListName already does catalog-then-snapshot with a docblock saying a missing name must never cost the row — wired to the audit writes and not to the display path. Record only. No code touched. npm run format:check clean.
The shots are regenerable from capture.spec.ts.txt and run to several megabytes of PNG. docs/design-sweep/ set the precedent: reports tracked, pixels not.
…reciting itself Five findings from the second design sweep's re-shoot of this page, all on /admin/access-lists. The three holder faults put one sentence and one link in a 78rem column. They now take `page--narrow`, which caps contents rather than the page box, so the h1's left edge and every rule's origin stay on the same vertical as the other admin routes — only the line length changes. Those same states rendered a "Watched lists" heading over a notice saying the heading had nothing under it. `showsObservations` is right that a stale answer beats a blank page, but only when there are rows; `showsRegion` adds the second half. The `ConfirmGroup`/`ConfirmingForm` pair deliberately stays OUTSIDE that gate — removing a list that has since left the catalog empties `compared` and `addable` in the same commit, so gating the pair would unmount the reporter on exactly the press it exists to report (the failure documented at admin/accounts/page.tsx:1075-1097). The "missing access" table printed one Corporation per row, which on the common case — one corp's members left off an alliance list — told the rows apart not at all. `sharedCorporation` measures the norm against the set the way `crewNorms` does on /account: state it once, list only what differs, and defeat the norm on a single unplaceable row rather than folding it in, because the sentence says "all of them". The broad-grants "plus an unknown number of others" clause moves out of the per-line map for the same reason; the wording is unchanged, so the promise is textually identical. "Stop watching" is the only control in a watched row, and `.btn--quiet` strips the border that says so — at rest it sat on the weight of the captions around it. Outline back, `.btn--quiet`'s restraint kept. No colour token added or changed; --rule and --ink-dim already existed. Also: a list watched but not yet read has no snapshot, so its row printed a bare `#4001` while the catalog it was picked from knew the name. Both now derive from the catalog-first rule `watchedListName` already used on the audit path. Six e2e tests, each shown to fail with its fix surgically removed (AGENTS.md item 22) — hardcoded page--wide, `showsRegion` widened to `showsObservations`, the catalog lookup dropped from `label`, `missingCorp` pinned to null, and the border-color declaration deleted. npm test 1476 passed. npx playwright test 405 passed. typecheck, lint, format:check, build all clean.
…t reach `<Submit>` exists for server actions. In a `<form method="get">` the submit is a document navigation, so `useFormStatus()` never reports pending: the button's `aria-busy` is fixed at "false" for the life of the document, and the guard's latch — taken synchronously on the first click, released only by watching pending go true and back — is never released. The ordinary path replaces the document before that matters, but a document that outlives its own navigation comes back with the latch set and the button silently refusing every press. The three GET filter forms get a plain `<button type="submit">` instead. Nothing rendered changes: they were passed the same className, and the only other output was the aria-busy that was always "false". They lose nothing either — re-running a filter is idempotent, which is why the guard had no work to do there in the first place. Also corrects the audit page's docblock, which named `Submit` as the reason that page is not JS-free. `Scroller` is.
…iewport `next/image` decides what to pull out of the srcset from `sizes` alone, and with none it assumes the image spans the viewport. The closing artwork draws at 420px (260px in the compact layout) and was being fetched at 1200 on a desktop — the heaviest asset on the page, at roughly three times the width it is drawn at, and worse on a phone where it matters more. The two values track `.closing img` and `.closing--compact img`, so the branch is computed once above and drives both the class and the hint; a `sizes` that outran the CSS would undo the point of the narrower frame. The test asserts the width the browser chose out of the srcset rather than the attribute, because the attribute is the input and the fetch is the cost. Control run with the prop removed: 1200 against a bound of 640.
Gaps 1, 2 and 4 were the shots the sweep never got. Two fixture corrections were all that stood in the way, and both were the same mistake — a fixture that wrote the output instead of asking the code for it: - Gap 1 wrote `amount` by hand, so every share came out flat and the roster's deviation channel had nothing to show. It now runs the real `recalculate`, with shares from 0.25 to 3.00 and one exclusion. - Gap 2 seeded action names absent from `PARTS`, so every Details cell rendered the generic key=value fallback — precisely the rendering that column was rebuilt to avoid. The shot proved nothing about the code that ships. Gap 1 closes clean: enumeration on that roster is warranted, item 12's fix holds against a real split. Gaps 2 and 4 produce four findings, recorded in SYNTHESIS.md — the largest being that `/payouts/new`'s rejection notice pushes the form 88px down, measured, and only when it has something to say. Nothing in `src/` is touched; the Phase 4 gate still stands. The spec is preserved as `capture-gaps-124.spec.ts.txt` beside `capture.spec.ts.txt`, same convention, so any of it can be re-shot.
`.form-stack` already reserved the notice slot out of flow, so an empty reservation cost nothing and the header sat flush — measured at 0px. But a populated `Notice` renders `.notice`, a different class from the empty `.notice-slot`, so the reset that flattened `.rule-head`'s 48px top margin stopped matching at exactly the moment there was something to say. Three spacings then stacked — the notice's own 24px bottom margin, the grid's 16px row gap, and the header's 48px — and the distance went 0px empty, 88px populated. Backwards: the one state where the error and the form it is about need to read as one thing was the state that pushed them a third of a phone screen apart, on /payouts/new, where vertical space is already scarce at 320px. Two rules. The reset gains a `.notice` branch. And `.notice`'s own bottom margin — written for flow layout, where it is the only thing separating the notice from what follows — is zeroed inside `.form-stack`, where the row gap already does that job. That second half is not specific to the header: all four forms with a `.form-stack` mount their `Notice` unconditionally, so add-participant, appraise and flat-pool each had 40px where every other sibling pair sits at 16px. The header case was just the loudest. The test asserts a distance rather than a computed style, because the defect was the sum and no single declaration was wrong on its own; the expected value is the form's own row gap, read off the container. It also pins the empty case at 0px, so a "fix" that reintroduces the reservation's spacing fails too. Removing either rule alone fails it — 48px with the reset branch gone, 24px with the margin restored.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 16 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThis change adds Design Sweep 2 documentation and capture fixtures. It also updates accessibility, responsive layouts, form validation, payout flows, admin pages, error recovery, shared UI components, styling, and E2E coverage. ChangesDesign Sweep 2
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1⚔️ Resolve merge conflicts 💡
Comment |
There was a problem hiding this comment.
Actionable comments posted: 19
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@DESIGN.md`:
- Around line 325-327: Update the transition-count statement in
docs/design-sweep-2/reports/record-contradiction.md to report ten declarations,
matching the ten transition declarations in src/app/globals.css; leave the
surrounding analysis unchanged.
In `@docs/design-sweep-2/BRIEF.md`:
- Around line 293-302: Include /admin/access-lists in the reviewed scope as a
route-enumeration addition. In docs/design-sweep-2/BRIEF.md lines 293-302,
replace the out-of-scope wording with that classification; in
docs/design-sweep-2/reports/access-lists-audit.md lines 3-7, remove the
out-of-scope classification; and in docs/design-sweep-2/SYNTHESIS.md lines
622-628, classify its findings as included review scope.
In `@docs/design-sweep-2/capture.spec.ts.txt`:
- Line 442: Update the account query assertion at the seeded account setup to
await db.select().from(account) and validate that the returned rows are
non-empty, rather than asserting the promise object is truthy.
In `@docs/design-sweep-2/reports/access-lists-critique.md`:
- Around line 3-5: Remove the stale “OUT-OF-SCOPE ADDITION” warning at the
beginning of the access-lists critique, and replace it with an accurate
statement that /admin/access-lists is included in the reviewed sweep, or omit
the banner entirely.
- Around line 252-256: Add the required attribute to the access-list select in
the relevant form within the page component, preserving the disabled, selected
empty placeholder option so native validation blocks submission until a real
list is chosen.
In `@docs/design-sweep-2/reports/audit-audit.md`:
- Around line 336-342: Update the discussion of NVDA and JAWS announcements in
the report to qualify it as an expectation based on accessible-name and
accessible-description computation, consistent with the stated lack of running
screen-reader validation; alternatively, add documented screen-reader evidence
supporting the claim.
In `@docs/design-sweep-2/reports/boundaries-audit.md`:
- Around line 127-134: Update finding 2 in the boundaries audit to reflect the
implemented retry handoff: remove the recommendation to focus `#main` before
reset() and remove the request to add the error-boundary success-path test,
since src/app/error.tsx already focuses the recovered page’s `#main` after unmount
and e2e/error-boundary.spec.ts already covers it.
- Around line 164-169: Move seenAt storage out of the remounted error component
in error.tsx, replacing the component-local useRef with a persistent owner or
capture at the error source. Preserve one ISO-8601 UTC timestamp per error
identity, retaining the first timestamp when no digest is available, across
reset()-triggered remounts.
In `@docs/design-sweep-2/reports/boundaries-critique.md`:
- Around line 80-118: The boundaries critique still contains obsolete focus-ring
claims and a recommendation already implemented by the existing
h1[tabindex="-1"] width rule. Update or remove the finding in the report,
including its claims about the missing ring and proposed fix, so it reflects the
current implementation before merging.
In `@docs/design-sweep-2/reports/login-audit.md`:
- Around line 372-380: Update the `.launch__foot` tracking declaration in the
CSS fix list to use the shared `var(--track-label)` token instead of the raw
`0.12em` value, and move this item out of “Contested” into the recommended CSS
fixes.
In `@docs/design-sweep-2/reports/payout-detail-audit.md`:
- Around line 188-232: Update the fix so every listed action has both feedback
paths covered: add suitable pendingLabel values to exclude, mark paid, revert,
remove, pool delete, finalize, unlock, roster replacement, and operation
deletion, and ensure refusal notifications reach a mounted ConfirmGroup or
explicit onRefused handler for each action. Pay particular attention to
pool-flow.tsx and the page-level roster and operation forms; alternatively
narrow the finding to only the controls the implementation actually covers.
In `@docs/design-sweep-2/reports/payout-detail-critique.md`:
- Around line 85-87: Update the 320px reflow discussion in the payout-detail
critique: either add a 320px capture and page-level reflow assertion to
capture.spec.ts.txt, then report its measured result, or mark the case as
unmeasured and remove the unsupported “where it will be worse” statement.
In `@docs/design-sweep-2/reports/payouts-audit.md`:
- Around line 11-18: Reconcile the payout counts across the audit report,
critique, and capture fixture by identifying the fixture version used for
04-payouts-full. Update the reported finalized-row and amber-token totals in the
affected report sections to match that fixture, or explicitly name the version
when retaining differing distributions; ensure all references consistently
describe the same 34-operation dataset.
In `@docs/design-sweep-2/reports/payouts-new-critique.md`:
- Around line 66-79: Correct the `rows={4}` / `rows={3}` alternative in the
payout critique so it no longer claims the change puts Create above the fold at
1440×900. Describe it as only a partial height reduction, or replace the claim
with an accurate measured outcome while preserving the Disclosure alternative
and its defaultOpen requirements.
In `@docs/design-sweep-2/SYNTHESIS.md`:
- Around line 7-8: Update the Phase 4 gate statement near the “Nothing in src/
has been modified” text to identify the baseline revision and clarify that the
claim applied to the Phase 3 snapshot, or replace it with the final disposition
based on the committed tree.
In `@e2e/audit.spec.ts`:
- Around line 1000-1008: Update the audit test around the actor-filter and retry
flow to assert the seeded character.linked audit entry and the expected
target-filtered entry content rather than relying on tbody row counts. Retain
the separate .log__empty assertion, since the empty state is also rendered as a
table row, and verify the retry result specifically reflects the actor-to-target
link.
In `@e2e/payouts.spec.ts`:
- Line 1108: Update the test date setup around the tomorrow constant to use a
date at least two UTC days in the future, ensuring it remains future-dated even
if the test crosses UTC midnight; leave the existing ISO date formatting intact.
In `@src/app/admin/audit/page.tsx`:
- Around line 52-58: Update generateMetadata in the audit page to use the shared
one() repeated-parameter behavior rather than the local helper that selects the
first value, so metadata matches the page body for repeated before parameters.
Add an E2E assertion covering ?before=abc&before=123 and the resulting paged
title.
In `@src/app/payouts/actions.ts`:
- Around line 260-280: Update createOperationAction to validate the complete
FormData payload through a single Zod schema before calling appraiseLoot or
createOperationWithContents. Move the occurredAt parsing and future-date
refinement into that schema, include every other action input currently passed
to the services, and return the existing validation failure shape when parsing
fails; remove the manual occurredAt-only validation path.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a06d4a18-12fc-4efd-84a0-d090d6020606
📒 Files selected for processing (65)
.gitignoreAGENTS.mdDESIGN.mdPRODUCT.mddocs/design-sweep-2/BRIEF.mddocs/design-sweep-2/SYNTHESIS.mddocs/design-sweep-2/capture-gaps-124.spec.ts.txtdocs/design-sweep-2/capture.spec.ts.txtdocs/design-sweep-2/reports/access-lists-audit.mddocs/design-sweep-2/reports/access-lists-critique.mddocs/design-sweep-2/reports/audit-audit.mddocs/design-sweep-2/reports/audit-critique.mddocs/design-sweep-2/reports/boundaries-audit.mddocs/design-sweep-2/reports/boundaries-critique.mddocs/design-sweep-2/reports/login-audit.mddocs/design-sweep-2/reports/login-critique.mddocs/design-sweep-2/reports/payout-detail-audit.mddocs/design-sweep-2/reports/payout-detail-critique.mddocs/design-sweep-2/reports/payouts-audit.mddocs/design-sweep-2/reports/payouts-critique.mddocs/design-sweep-2/reports/payouts-new-audit.mddocs/design-sweep-2/reports/payouts-new-critique.mddocs/design-sweep-2/reports/record-contradiction.mddocs/design-sweep-2/reports/sync-audit.mddocs/design-sweep-2/reports/sync-critique.mddocs/design-sweep-2/reports/whole-app.mddocs/design-sweep-2/skill-update.patche2e/access-lists.spec.tse2e/account.spec.tse2e/audit.spec.tse2e/error-boundary.spec.tse2e/login.spec.tse2e/not-found.spec.tse2e/payouts.spec.tse2e/sync.spec.tssrc/app/_components/confirm-notice.tsxsrc/app/_components/confirm-submit.tsxsrc/app/_components/focus-heading.tsxsrc/app/_components/nav-items.tssrc/app/_components/submit-guard.tssrc/app/_components/submit.tsxsrc/app/_components/ui.tsxsrc/app/account/page.tsxsrc/app/admin/access-lists/page.tsxsrc/app/admin/access-lists/view.tssrc/app/admin/accounts/page.tsxsrc/app/admin/audit/page.tsxsrc/app/admin/sync/page.tsxsrc/app/admin/sync/view.tssrc/app/error.tsxsrc/app/globals.csssrc/app/layout.tsxsrc/app/login/page.tsxsrc/app/payouts/[id]/inline-edit.tsxsrc/app/payouts/[id]/lifecycle-submit.tsxsrc/app/payouts/[id]/page.tsxsrc/app/payouts/[id]/pay-flow.tsxsrc/app/payouts/[id]/payment-history.tsxsrc/app/payouts/[id]/pool-flow.tsxsrc/app/payouts/actions.tssrc/app/payouts/errors.tssrc/app/payouts/new/new-operation-form.tsxsrc/app/payouts/new/page.tsxsrc/app/payouts/page.tsxtests/payment-history.test.ts
| ### 8. `/admin/access-lists` — **register: product** | ||
|
|
||
| - Source: `src/app/admin/access-lists/page.tsx` | ||
| - Shots: `14-access-lists.*` | ||
| - **This surface appears in neither the owner's scope list nor the Aug-5 sweep.** | ||
| It has never been reviewed. Mark your report as covering an out-of-scope | ||
| addition so its findings can be separated cleanly if the owner does not want | ||
| them. | ||
| - Report: `docs/design-sweep-2/reports/access-lists-critique.md` / | ||
| `access-lists-audit.md` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Treat /admin/access-lists as included scope.
The updated sweep rule says route enumeration adds an omitted surface to the review. Do not retain an out-of-scope banner for this surface.
docs/design-sweep-2/BRIEF.md#L293-L302: describe the surface as added by route enumeration.docs/design-sweep-2/reports/access-lists-audit.md#L3-L7: remove the out-of-scope classification.docs/design-sweep-2/SYNTHESIS.md#L622-L628: classify the findings as included review scope.
📍 Affects 3 files
docs/design-sweep-2/BRIEF.md#L293-L302(this comment)docs/design-sweep-2/reports/access-lists-audit.md#L3-L7docs/design-sweep-2/SYNTHESIS.md#L622-L628
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/design-sweep-2/BRIEF.md` around lines 293 - 302, Include
/admin/access-lists in the reviewed scope as a route-enumeration addition. In
docs/design-sweep-2/BRIEF.md lines 293-302, replace the out-of-scope wording
with that classification; in docs/design-sweep-2/reports/access-lists-audit.md
lines 3-7, remove the out-of-scope classification; and in
docs/design-sweep-2/SYNTHESIS.md lines 622-628, classify its findings as
included review scope.
| await page.goto("/admin/accounts"); | ||
| await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); | ||
| await shoot(page, "15-admin-accounts"); | ||
| await expect(db.select().from(account)).toBeTruthy(); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Await and assert the seeded account rows.
Line 442 asserts that a promise object is truthy. It does not execute or validate the query result. The capture can pass after broken account setup. Await the query and assert a non-empty result.
Proposed fix
- await expect(db.select().from(account)).toBeTruthy();
+ const accounts = await db.select().from(account);
+ expect(accounts).not.toHaveLength(0);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await expect(db.select().from(account)).toBeTruthy(); | |
| const accounts = await db.select().from(account); | |
| expect(accounts).not.toHaveLength(0); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/design-sweep-2/capture.spec.ts.txt` at line 442, Update the account
query assertion at the seeded account setup to await db.select().from(account)
and validate that the returned rows are non-empty, rather than asserting the
promise object is truthy.
| > **OUT-OF-SCOPE ADDITION.** This surface appears in neither the owner's scope | ||
| > list for this sweep nor the Aug-5 sweep. It has never been reviewed. Every | ||
| > finding below can be lifted out cleanly if the owner does not want them. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove the stale out-of-scope warning.
The PR objectives explicitly include /admin/access-lists in this sweep. This banner says the surface was not reviewed and may cause maintainers to discard valid findings. Replace it with the intended scope statement or remove it.
As per PR objectives, /admin/access-lists is one of the reviewed surfaces.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/design-sweep-2/reports/access-lists-critique.md` around lines 3 - 5,
Remove the stale “OUT-OF-SCOPE ADDITION” warning at the beginning of the
access-lists critique, and replace it with an accurate statement that
/admin/access-lists is included in the reviewed sweep, or omit the banner
entirely.
| **Nothing in `src/` has been modified.** This document is the Phase 3 output and | ||
| the Phase 4 gate: the owner chooses what gets worked before any edit. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Pin the Phase 3 source snapshot.
src/ changes in this PR. These lines are false when read against the committed tree. Name the baseline revision and state that this was true at the Phase 3 snapshot, or replace the gate language with the final disposition.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/design-sweep-2/SYNTHESIS.md` around lines 7 - 8, Update the Phase 4 gate
statement near the “Nothing in src/ has been modified” text to identify the
baseline revision and clarify that the claim applied to the Phase 3 snapshot, or
replace it with the final disposition based on the committed tree.
| // `max={today}` on the form declares this rule; until now the browser was the | ||
| // only thing enforcing it, so a hand-built request could always date an | ||
| // operation into the future. That became load-bearing the moment this form | ||
| // took `noValidate` (see new-operation-form.tsx): with native validation off, | ||
| // the attribute stops being enforcement at all and this check is the rule. | ||
| // | ||
| // Compared against the same UTC-midnight boundary `parseYmd` produces, not | ||
| // against `now`: both sides are then EVE-day granular, so an operation | ||
| // recorded during today's downtime is not rejected for being "ahead" of an | ||
| // instant a few hours later in the same day. | ||
| // | ||
| // `/payouts/[id]`'s own date field is deliberately NOT changed to match. It | ||
| // still runs native validation, so its `max={today}` still holds for anyone | ||
| // using the page; it carries the same scripted-request gap this closes here, | ||
| // pre-existing and unchanged, and closing it there means a new code on | ||
| // `OPERATION_ERRORS` for a path this task did not touch. | ||
| const todayUtc = new Date(); | ||
| todayUtc.setUTCHours(0, 0, 0, 0); | ||
| if (occurredAt.getTime() > todayUtc.getTime()) { | ||
| return { ok: false, code: "date_future" }; | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Validate the complete form with Zod before service calls.
createOperationAction manually parses occurredAt and continues with other unvalidated FormData fields. Parse one Zod schema, including the future-date refinement, before appraiseLoot and createOperationWithContents.
As per path instructions, “Server actions must validate their input with zod before touching a service.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/payouts/actions.ts` around lines 260 - 280, Update
createOperationAction to validate the complete FormData payload through a single
Zod schema before calling appraiseLoot or createOperationWithContents. Move the
occurredAt parsing and future-date refinement into that schema, include every
other action input currently passed to the services, and return the existing
validation failure shape when parsing fails; remove the manual occurredAt-only
validation path.
Source: Path instructions
…-2026-08-10 # Conflicts: # e2e/access-lists.spec.ts # src/app/admin/access-lists/page.tsx
|
@coderabbitai full review |
|
`generateMetadata` had its own `one()` that took the FIRST value of a repeated query param, shadowing the module's, which takes the last. On `?before=abc&before=99999999` the body paged on the numeric cursor while the title announced an unpaged log — and on this surface the title IS the announcement, because every control that changes the result set is a document load. The `paged` guard exists precisely to stop the title claiming a page the admin is not on, and the shadow defeated it. Also two test fixes CodeRabbit was right about: - The future-date test built its date 24h out and the action compares against UTC midnight of the instant it runs, so a run that crossed the boundary between fill and submit was posting today and passing. 48h. - The actor-filter test asserted row cardinality only, which any single row satisfies. It now names which entries it expects on each side of the nudge, which is the behaviour the test is about.
A full design sweep of the app — eight surfaces in scope plus
/admin/access-lists,eighteen reviewer reports, and the fixes chosen off the ranked backlog in
docs/design-sweep-2/SYNTHESIS.md. That document is the record: what was found,what it costs a user, what was worked, and what was deliberately left.
Nothing here changes a colour token, a migration, or persisted data.
What got fixed
Layout that fails on a phone.
/payouts/[id]was the only surface whosenarrow capture was wider than the viewport — 771px against 390px — from two
independent one-line causes.
/payouts' pinned Name column measured 69px of a286px region, so the column that exists to keep a payout identifiable while
scrolling didn't; it now reflows to labelled blocks the way
.log--crewalreadydoes.
/admin/sync's housekeeping summary had never fitted at 320px in anystate, healthy included.
Forms that promised things they couldn't deliver. Four of
/payouts/new'sfive error messages were unreachable through the form — native validation ran
ahead of the event React's
<form action>fires from, so the app's own sentenceswere server backstops for scripted requests only. And the three GET filter forms
used
<Submit>, whose pending state cannot arrive during a document navigationand whose guard latch is never released — a document that outlives its own
navigation came back with a permanently dead button.
Rejections that drifted away from their forms. The last fix in the branch:
a populated
Noticerenders a different class from the empty reserved slot, sothe spacing reset stopped matching at exactly the moment there was something to
say. Measured 0px empty, 88px populated.
Recital.
/payoutsand/admin/auditeach printed one fact about the wholeset on every row. Both now state it once.
Six rules the design record states and the code disproved. A dedicated
reviewer read DESIGN.md against the implementation; drift never announces itself,
and some of it had survived multiple sweeps.
Plus accessibility fixes (three controls misreporting what they are, an inert
skip link, a retry that dropped focus to
<body>on success), and/admin/access-lists' fault states, which were reviewed unrequested but whosefindings lift out cleanly.
Verification
typecheck,lint,format:check,npm test(1476 passed, 92 files), the fullPlaywright suite (407 passed), and
npm run build— all run on the final tree.Every test written to prove a fix was shown to fail with only the code under test
removed. That practice is now written into AGENTS.md as item 22, after a control
run in this branch turned out to be inert and to have been passing for the wrong
reason.
What was found and not worked
Recorded in SYNTHESIS.md rather than silently dropped:
/admin/audit's Details column prints raw character IDs where the Targetcolumn three cells left resolves the same referent to a name. Real scope — a
new key map, a new part, a widened fetch. Being handled separately.
payout.deletedclips mid-number at 1440px; the disclosure recovers it.#0a0a0aground is PRODUCT.md's own anti-reference, and the whole-appreviewer ranks it third by cost. Outside the colour freeze this sweep ran
under, so it is surfaced for a decision rather than acted on.
themeColorliteralfrom the retired navy palette,
paidandunpaidrendering at the samecolour on the roster, and
.escalation's 1.62:1 border. All are token useor assignment changes, not value changes. Owner's call.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation